package cn.caplike.demo.spring.security.dynamic.authorization.configuration.security.filter;

import cn.caplike.data.redis.service.spring.boot.starter.RedisKey;
import cn.caplike.data.redis.service.spring.boot.starter.RedisService;
import cn.caplike.demo.spring.security.dynamic.authorization.configuration.security.SecurityConfiguration;
import cn.caplike.demo.spring.security.dynamic.authorization.domain.dto.CustomUserDetailsDto;
import cn.caplike.demo.spring.security.dynamic.authorization.domain.entity.User;
import cn.caplike.demo.spring.security.dynamic.authorization.util.AccessTokenUtils;
import cn.caplike.demo.spring.security.dynamic.authorization.util.ResponseUtils;
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * {@link UsernamePasswordAuthenticationFilter} <br>
 * Qualified ENDPOINT: /auth/login <br>
 * -> Filter 执行顺序: HttpServletRequestWrapFilter ---> CsrfFilter ---> JWTAuthenticationFilter ---> JWTAuthorizationFilter <br>
 * -> 每次成功的登陆都应该生成 access-token
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-05-07 16:25
 */
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private static final String AUTHENTICATION_SUCCESS_MESSAGE = "Login success.";

    private static final String AUTHENTICATION_INCOMPLETE_MESSAGE = "User credential is null.";

    private static final String USERNAME_PARAMETER_NAME = "name";

    private static final String CSRF_TOKEN = SecurityConfiguration.CSRF_TOKEN;

    private static final String ACCESS_TOKEN = SecurityConfiguration.ACCESS_TOKEN;

    /**
     * {@link AuthenticationManager }
     */
    private final AuthenticationManager authenticationManager;

    /**
     * {@link RedisService }
     */
    private final RedisService redisService;

    private User user;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, RedisService redisService) {
        this.authenticationManager = authenticationManager;
        this.redisService = redisService;

        // 当 requestUrl 是 /auth/login 时会经过这个过滤器
        super.setFilterProcessesUrl(SecurityConfiguration.LOGIN_URI);
        super.setUsernameParameter(USERNAME_PARAMETER_NAME);
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 数据是通过 requestBody 传输
        this.user = JSON.parseObject(request.getInputStream(), StandardCharsets.UTF_8, User.class);

        if (Objects.isNull(user)) {
            ResponseUtils.forbiddenResponse(response, AUTHENTICATION_INCOMPLETE_MESSAGE);
            return null;
        }

        try {
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword())
            );
        } catch (AuthenticationException authenticationException) {
            unsuccessfulAuthentication(request, response, authenticationException);

            // 终止后续执行
            return null;
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
        log.debug("Successful authentication.");

        SecurityContextHolder.getContext().setAuthentication(authResult);
        final CustomUserDetailsDto customUserDetailsDto = (CustomUserDetailsDto) authResult.getPrincipal();

        // ~ 登陆成功后, 给响应头置入 access-token 和 csrf-token

        response.setHeader(
                ACCESS_TOKEN,
                // 缓存 access-token
                redisService.setValue(
                        RedisKey.builder().prefix(customUserDetailsDto.getName()).suffix(ACCESS_TOKEN).build(),
                        AccessTokenUtils.create(customUserDetailsDto),
                        AccessTokenUtils.LIFE_TIME
                )
        );

        response.setHeader(
                CSRF_TOKEN,
                redisService.getValue(
                        RedisKey.builder().prefix(customUserDetailsDto.getName()).suffix(CSRF_TOKEN).build(),
                        String.class
                )
        );

        ResponseUtils.okResponse(response, AUTHENTICATION_SUCCESS_MESSAGE);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
        log.debug("Unsuccessful authentication : delete cached csrf-token.");

        // 如果登陆失败, 清楚在 CsrfFilter 阶段已经缓存的无效的 csrf-token, 不会有 csrf-token 返回
        redisService.delete(RedisKey.builder().prefix(user.getName()).suffix(SecurityConfiguration.CSRF_TOKEN).build());

        ResponseUtils.forbiddenResponse(response, failed.getMessage());
    }
}
